Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cross-program invocations #9582

Merged
merged 3 commits into from
Apr 28, 2020
Merged

Add Cross-program invocations #9582

merged 3 commits into from
Apr 28, 2020

Conversation

jackcmay
Copy link
Contributor

Problem

  • Programs cannot invoke other programs
  • Programs cannot generate their own signatures in process_instruction as defined in the Cross-Program Invocations design.

Summary of Changes

Implements the following proposals

This PR has been whittled down to the essential changes by breaking up all dependent and related changes into other PRs that have already been committed.

Fixes #

@jackcmay jackcmay added the work in progress This isn't quite right yet label Apr 20, 2020
@jackcmay
Copy link
Contributor Author

Hitting an error loading the solana_bpf_loader_program on linux:
[2020-04-20T17:19:33.608471890Z WARN solana_runtime::native_loader] Failed to load: Custom { kind: Other, error: "/solana/target/debug/deps/libsolana_bpf_loader_program.so: cannot allocate memory in static TLS block" }

Debugging... wip label for now

@jackcmay jackcmay removed the work in progress This isn't quite right yet label Apr 21, 2020
@codecov
Copy link

codecov bot commented Apr 21, 2020

Codecov Report

Merging #9582 into master will decrease coverage by 0.2%.
The diff coverage is 61.2%.

@@           Coverage Diff            @@
##           master   #9582     +/-   ##
========================================
- Coverage    80.5%   80.3%   -0.3%     
========================================
  Files         280     281      +1     
  Lines       64031   64615    +584     
========================================
+ Hits        51592   51897    +305     
- Misses      12439   12718    +279     

@jackcmay
Copy link
Contributor Author

jackcmay commented Apr 21, 2020

Fixed and ready for review, there's a lot in there so let the critiques commence!

Copy link
Member

@mvines mvines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work!

programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
garious
garious previously approved these changes Apr 22, 2020
Copy link
Contributor

@garious garious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me. I found the term "cross-program" to be a little confusing. I think it's used to mean at least "foreign function call" and "cross-program process_instruction". Even though I've been calling the function process_instruction(), it looks like calling it invoke() would nicely disambiguate it from MessageProcessor::process_instruction() and allow you to use "invoke" or "invocation" in places you're currently using "cross_program". Maybe even "solana_ffi::invoke".

As for the new runtime rules, I didn't review it in detail, but it all looked consistent with the design and what we've discussed on Discord. And lots of tests, so not a lot to worry about. Hoping we can get this merged soon and into v1.2.0.

@jackcmay
Copy link
Contributor Author

@garious I'm a fan of process_instruction() since it both conveys what is happening (processing the provided instruction) and is consistent with the rest of the runtime. The term "cross-program" I could take or leave. I like "solana_ffi" though it seems to imply a lot more then we need or want to. Calling out to other programs is natural and in our case there isn't anything to be gained by letting the caller know that it might be calling another language. Maybe invocation::process_instruction()?

@garious
Copy link
Contributor

garious commented Apr 22, 2020

@jackcmay, how about solana_invoke::process_instruction?

@garious
Copy link
Contributor

garious commented Apr 22, 2020

solana_call?

@jackcmay
Copy link
Contributor Author

@greg The full path would be comprised of solana_sdk:: followed by the file name followed by the name of the function. So for example:

  • solana_sdk::invoke::process_instruction
  • solana_sdk::invocation::process_instruction

Did you have something different in mind?

@garious
Copy link
Contributor

garious commented Apr 22, 2020

@jackcmay Got it. I missed that. What about solana_sdk::program::process_instruction()?

@jackcmay
Copy link
Contributor Author

Originally had it in solana_sdk::program_utils::process_instruction() but that would require featurizing those functions rather than featurize the whole file like in solana_sdk::cross_program. I try to avoid speckled featurization is possible. program seems too generic and using process_instruction does conflict with a program's entrypoint function which we usually call process_instruction.

Along your lines of thinking we could do: solana_sdk::program::invoke

@garious
Copy link
Contributor

garious commented Apr 22, 2020

solana_sdk::program::invoke

I like that a lot

programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
programs/bpf/rust/invoked/src/instruction.rs Outdated Show resolved Hide resolved

// Process instruction

let program_account = (*accounts[program_id_index]).clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only need to clone the program data if the program is also an instruction account.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you referring to as an instruction account?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant an account that is passed as an argument to an instruction

programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
programs/bpf_loader/src/helpers.rs Outdated Show resolved Hide resolved
const DERIVED_KEY2_INDEX: usize = 7;

entrypoint!(process_instruction);
fn process_instruction(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a test for cross program instructions with only a subset of the caller accounts.

Also, is there a min / max number of accounts that can be passed to cross_program::process_instruction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea will add those tests. Also, there is currently no limit to the number of accounts that can be passed beside the memory restrictions of the calling process. This could be a ddos vulnerability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, is there a reason to do this? The runtime only passes the accounts listed in the instruction to the invoked program. Breaking up the accounts vector isn't a fun thing to do

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok maybe I'm missing something. What if a program is called with 2 account arguments and wants to call a program that needs 3?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it fails, programs need to be passed all the accounts it needs to pass to any invokes calls it intends to make

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I think I figured out the disconnect!

I was imagining that the caller program could rearrange, filter, dupe accounts etc. and then pass that new account slice to the called function. But that's overkill because as you mentioned, the cross program instruction just needs to reference the indices of the accounts from the original account slice.

As it stands, the caller could still choose to pass only a subset of the accounts to the called program I think. I don't see a check to make sure that all accounts in pre_accounts are present in the accounts slice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that the caller needs to pass the accounts slice back out through sol_process_signed_instruction_? Can't we check for them how accounts have been modified?

Copy link
Member

@jstarry jstarry Apr 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh we are doing that. Sorry!!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no we aren't actually 😅

Does this make sense? Do we need the caller to pass through all the account infos? Don't we already know where the accounts are memory mapped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue is summarized in #9711 and is planned as a followup change to the PR

sdk/src/cross_program.rs Outdated Show resolved Hide resolved
runtime/src/message_processor.rs Outdated Show resolved Hide resolved
runtime/src/message_processor.rs Show resolved Hide resolved
Copy link
Member

@jstarry jstarry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like malicious programs could read the entirety of a memory region if they mess with the serialized lengths. Is that problematic?

@jackcmay
Copy link
Contributor Author

@jstarry We can't enforce that the passed buffers are sized correctly or even pointing to anything valid. We can only verify that the pointer and size of the buffer have been granted to the calling program. The called program only has access to what has been passed by the caller, aka the called can't read or write memory of the caller's address space outside of what the caller passed.

@jstarry
Copy link
Member

jstarry commented Apr 24, 2020

Ok, sounds good!

@mergify mergify bot dismissed garious’s stale review April 27, 2020 19:31

Pull request has been modified.

@jackcmay
Copy link
Contributor Author

solana_sdk::program::invoke

I like that a lot

@garious Take a look at the new naming, like how it looks now that its in?

@jackcmay
Copy link
Contributor Author

jackcmay commented Apr 28, 2020

@mvines @jstarry @garious
I disabled reentrancy in this PR, we can open it up again when we work out those policy details. Here is what the reentrancy check looks like, self.program_ids is the current call stack of program ids

        if self.program_ids.contains(key) && self.program_ids.last() != Some(key) {
            // Reentrancy not allowed unless caller is calling itself
            return Err(InstructionError::ReentrancyNotAllowed);
        }

Here are also some follow-up issues that came out of the review or todos:

Anything else?

@garious
Copy link
Contributor

garious commented Apr 28, 2020

@garious Take a look at the new naming, like how it looks now that its in?

Love it!

@jackcmay jackcmay merged commit 068f12f into solana-labs:master Apr 28, 2020
@jackcmay jackcmay deleted the cpi branch April 28, 2020 21:34
// Verify the called program has not misbehaved
result = invoke_context.verify_and_update(message, instruction, signers, accounts);
}
invoke_context.pop();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jackcmay Sorry for being after-party comment, but I just started to looking the program-related code. :)

This invoke_context.pop() isn't always be paired with invoke_context.push(...) when keyed_accounts[0].owner()? fails to borrow. Is that ok?

And qq: is cross-program invocation can be gracefully handled inside a invoker program in some way when invokee's result is InstructionError?
Then, this verification might start to reference a wrong program_id.

I think to it's better to avoid these kind of things like with closure surrounded by push() and pop()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that will never fail in this implementation so I left it. I recently updated this code to extend cross-program invocation to native programs and in those changes the case you mention no longer exists.

I'm not quite following the "qq", if the invoke fails the result is held and context poped before returning. Can you elaborate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants